﻿using System;
using System.Collections.Generic;
using System.Reflection;
using System.Workflow.ComponentModel;
using System.Workflow.Runtime.Tracking;
using System.Workflow.Runtime;

namespace Argos.Tracking
{
	/// <summary>
	/// A tracking service base class that is used to track changes in property values
	/// on workflow instances.
	/// </summary>
	/// <remarks>
	/// <para>
	/// When extending this class, you should normally only override the methods
	/// <see cref="IsPropertyTracked(PropertyInfo)"/>, <see cref="IsWorkflowInstanceTracked(Guid, Type)"/>
	/// and <see cref="OnWorkflowPropertyChanged(WorkflowPropertyChangedEventArgs)"/>.
	/// </para>
	/// </remarks>
	public abstract class PropertyTrackingServiceBase : TrackingService
	{

		#region #ctor: protected PropertyTrackingServiceBase()

		/// <summary>
		/// Creates a new instance of the class.
		/// </summary>
		protected PropertyTrackingServiceBase() { }

		#endregion



		#region public event EventHandler<WorkflowPropertiesRemovingEventArgs> WorkflowPropertiesRemoving

		private EventHandler<WorkflowPropertiesRemovingEventArgs> WorkflowPropertiesRemovingHandler;

		/// <summary>
		/// Fired when the properties of a workflow instance are about to be removed
		/// from the property tracking service.
		/// </summary>
		/// <remarks>
		/// Properties for a workflow instance are removed when the workflow instance
		/// completes or terminates.
		/// </remarks>
		public event EventHandler<WorkflowPropertiesRemovingEventArgs> WorkflowPropertiesRemoving
		{
			add { this.WorkflowPropertiesRemovingHandler += value; }
			remove { this.WorkflowPropertiesRemovingHandler -= value; }
		}

		#endregion
		
		#region public event EventHandler<WorkflowPropertyChangedEventArgs> WorkflowPropertyChanged

		private EventHandler<WorkflowPropertyChangedEventArgs> WorkflowPropertyChangedHandler;

		/// <summary>
		/// Fired when a workflow instance property has changed.
		/// </summary>
		public event EventHandler<WorkflowPropertyChangedEventArgs> WorkflowPropertyChanged
		{
			add { this.WorkflowPropertyChangedHandler += value; }
			remove { this.WorkflowPropertyChangedHandler -= value; }
		}

		#endregion



		private Dictionary<Guid, Dictionary<string, object>> WorkflowProperties = new Dictionary<Guid, Dictionary<string, object>>();

		private object SyncRoot = new object();



		#region public Dictionary<string, object> GetWorkflowProperties(Guid instanceId)

		/// <summary>
		/// Returns a dictionary containing all property values for the given workflow instnace.
		/// </summary>
		/// <param name="instanceId">The ID of the workflow instance whose properties to return.</param>
		public Dictionary<string, object> GetWorkflowProperties(Guid instanceId)
		{
			if (this.WorkflowProperties.ContainsKey(instanceId))
			{
				return new Dictionary<string, object>(this.WorkflowProperties[instanceId]);
			}

			return new Dictionary<string, object>();
		}

		#endregion

		#region public object GetWorkflowPropertyValue(Guid instanceId, string propertyName)

		/// <summary>
		/// Returns the value of the given property from the given workflow instance.
		/// </summary>
		/// <param name="instanceId">The ID of the workflow instance whose property value to return.</param>
		/// <param name="propertyName">The name of the property whose value to return.</param>
		/// <returns>Returns the value of the property. If the given instance ID or property name is not found, <c>null</c> is returned.</returns>
		public object GetWorkflowPropertyValue(Guid instanceId, string propertyName)
		{
			if (this.WorkflowProperties.ContainsKey(instanceId))
			{
				if (this.WorkflowProperties[instanceId].ContainsKey(propertyName))
				{
					return this.WorkflowProperties[instanceId][propertyName];
				}
			}

			return null;
		}

		#endregion


		#region protected override TrackingProfile GetProfile(Guid workflowInstanceId)

		protected override TrackingProfile GetProfile(Guid workflowInstanceId)
		{
			WorkflowInstance instance = this.Runtime.TryGetWorkflow(workflowInstanceId);
			if (null != instance)
			{
				Type workflowType = instance.GetWorkflowDefinition().GetType();
				if (this.IsWorkflowInstanceTracked(workflowInstanceId, workflowType))
				{
					return this.GetProfile(workflowType);
				}
			}
			return null;
		}

		#endregion

		#region protected override TrackingProfile GetProfile(Type workflowType, Version profileVersionId)

		protected override TrackingProfile GetProfile(Type workflowType, Version profileVersionId)
		{
			if (this.IsWorkflowInstanceTracked(Guid.Empty, workflowType))
				return this.GetProfile(workflowType);

			return null;
		}

		#endregion

		#region protected override TrackingChannel GetTrackingChannel(TrackingParameters parameters)

		protected override TrackingChannel GetTrackingChannel(TrackingParameters parameters)
		{
			return new Channel(parameters, this);
		}

		#endregion


		
		#region protected virtual bool IsPropertyTracked(PropertyInfo property)

		/// <summary>
		/// When overridden in a class, returns true if value changes to the given property
		/// should be tracked by the current tracking service.
		/// </summary>
		/// <param name="property">The property definition to track or not to track.</param>
		protected virtual bool IsPropertyTracked(PropertyInfo property)
		{
			return true;
		}

		#endregion

		#region protected virtual bool IsWorkflowInstanceTracked(Guid instanceId, Type workflowType)

		/// <summary>
		/// When overridden in a class, returns true if the tracking service should track
		/// property changes to the workflow with the given id and of the given type.
		/// </summary>
		/// <param name="instanceId">The ID of the workflow instance to check whether to track.</param>
		/// <param name="workflowType">The type of the workflow to check whether to track.</param>
		protected virtual bool IsWorkflowInstanceTracked(Guid instanceId, Type workflowType)
		{
			return true;
		}

		#endregion

		#region protected virtual void OnWorkflowPropertiesRemoving(WorkflowPropertiesRemovingEventArgs e)

		/// <summary>
		/// Fires the <see cref="WorkflowPropertiesRemoving"/> event.
		/// </summary>
		protected virtual void OnWorkflowPropertiesRemoving(WorkflowPropertiesRemovingEventArgs e)
		{
			if (null != this.WorkflowPropertiesRemovingHandler)
			{
				this.WorkflowPropertiesRemovingHandler.Invoke(this, e);
			}
		}

		#endregion
	
		#region protected virtual void OnWorkflowPropertyChanged(WorkflowPropertyChangedEventArgs e)

		/// <summary>
		/// Fires the <see cref="WorkflowPropertyChanged"/> event.
		/// </summary>
		protected virtual void OnWorkflowPropertyChanged(WorkflowPropertyChangedEventArgs e)
		{
			if (null != this.WorkflowPropertyChangedHandler)
			{
				this.WorkflowPropertyChangedHandler.Invoke(this, e);
			}
		}

		#endregion		

		#region protected override bool TryGetProfile(Type workflowType, out TrackingProfile profile)

		protected override bool TryGetProfile(Type workflowType, out TrackingProfile profile)
		{
			profile = this.GetProfile(workflowType);
			return true;
		}

		#endregion

		#region protected override bool TryReloadProfile(Type workflowType, Guid workflowInstanceId, out TrackingProfile profile)

		protected override bool TryReloadProfile(Type workflowType, Guid workflowInstanceId, out TrackingProfile profile)
		{
			profile = null;
			return false;
		}

		#endregion



		#region private TrackingProfile GetProfile(Type workflowType)

		private TrackingProfile GetProfile(Type workflowType)
		{
			TrackingProfile profile = new TrackingProfile();
			profile.Version = new Version(1, 0, 0);

			ActivityTrackPoint atp = new ActivityTrackPoint();

			List<ActivityExecutionStatus> statusList = new List<ActivityExecutionStatus>();
			atp.MatchingLocations.Add(new ActivityTrackingLocation(typeof(Activity), true, new ActivityExecutionStatus[] { ActivityExecutionStatus.Closed }));

			foreach (PropertyInfo pinfo in workflowType.GetProperties())
			{
				if (this.IsPropertyTracked(pinfo))
				{
					atp.Extracts.Add(new WorkflowDataTrackingExtract(pinfo.Name));
				}
			}

			profile.ActivityTrackPoints.Add(atp);

			return profile;
		}

		#endregion

		#region private void RemoveWorkflowProperties(Guid instanceId)

		private void RemoveWorkflowProperties(Guid instanceId)
		{
			try
			{
				this.OnWorkflowPropertiesRemoving(new WorkflowPropertiesRemovingEventArgs(instanceId));
			}
			catch { }

			lock (this.SyncRoot)
			{
				if (this.WorkflowProperties.ContainsKey(instanceId))
				{
					this.WorkflowProperties.Remove(instanceId);
				}
			}
		}

		#endregion
		
		#region private void SetWorkflowProperties(Guid instanceId, IDictionary<string, object> properties)

		private void SetWorkflowProperties(Guid instanceId, IDictionary<string, object> properties)
		{
			Dictionary<string, object> InstanceProperties = null;

			lock (this.SyncRoot)
			{
				if (this.WorkflowProperties.ContainsKey(instanceId))
				{
					InstanceProperties = this.WorkflowProperties[instanceId];
				}
				else
				{
					InstanceProperties = new Dictionary<string, object>();
					this.WorkflowProperties.Add(instanceId, InstanceProperties);
				}
			}


			List<WorkflowPropertyChangedEventArgs> changes = new List<WorkflowPropertyChangedEventArgs>();

			lock (this.SyncRoot)
			{
				foreach (string key in properties.Keys)
				{
					if (InstanceProperties.ContainsKey(key))
					{
						if (!object.Equals(properties[key], InstanceProperties[key]))
						{
							InstanceProperties[key] = properties[key];
							changes.Add(new WorkflowPropertyChangedEventArgs(instanceId, key, properties[key]));
						}
					}
					else
					{
						InstanceProperties.Add(key, properties[key]);
						changes.Add(new WorkflowPropertyChangedEventArgs(instanceId, key, properties[key]));
					}
				}
			}

			foreach (var item in changes)
			{
				this.OnWorkflowPropertyChanged(item);
			}
		}

		#endregion



		private class Channel : TrackingChannel
		{

			#region #ctor: public Channel(TrackingParameters parameters, PropertyTrackingServiceBase service)

			public Channel(TrackingParameters parameters, PropertyTrackingServiceBase service)
			{
				this.Parameters = parameters;
				this.Service = service;
			}

			#endregion
			
			private TrackingParameters Parameters;
			private PropertyTrackingServiceBase Service;



			#region protected override void InstanceCompletedOrTerminated()

			protected override void InstanceCompletedOrTerminated()
			{
				this.Service.RemoveWorkflowProperties(this.Parameters.InstanceId);
			}

			#endregion
			
			#region protected override void Send(TrackingRecord record)

			protected override void Send(TrackingRecord record)
			{
				ActivityTrackingRecord atr = record as ActivityTrackingRecord;
				if (null != atr)
				{
					Dictionary<string, object> properties = new Dictionary<string, object>();
					foreach (TrackingDataItem itm in atr.Body)
					{
						properties.Add(itm.FieldName, itm.Data);
					}
					this.Service.SetWorkflowProperties(this.Parameters.InstanceId, properties);
				}
			}

			#endregion

		}

	}


	/// <summary>
	/// The event arguments class for the <see cref="PropertyTrackingServiceBase.WorkflowPropertyChanged"/> event.
	/// </summary>
	public sealed class WorkflowPropertyChangedEventArgs : EventArgs
	{

		#region public WorkflowPropertyChangedEventArgs(Guid instanceId, string propertyName, object propertyValue)

		/// <summary>
		/// Creates a new instance and sets the properties of the class.
		/// </summary>
		/// <param name="instanceId">The value for the <see cref="InstanceId"/> property.</param>
		/// <param name="propertyName">The value for the <see cref="PropertyName"/> property.</param>
		/// <param name="propertyValue">The value for the <see cref="PropertyVaue"/> property.</param>
		public WorkflowPropertyChangedEventArgs(Guid instanceId, string propertyName, object propertyValue)
		{
			this.InstanceId = instanceId;
			this.PropertyName = propertyName;
			this.PropertyValue = propertyValue;
		}

		#endregion



		#region public Guid InstanceId { get; private set; }

		/// <summary>
		/// Returns the ID of the workflow instance whose property has changed.
		/// </summary>
		public Guid InstanceId { get; private set; }

		#endregion

		#region public string PropertyName { get; private set; }

		/// <summary>
		/// Returns the name of the property that has changed.
		/// </summary>
		public string PropertyName { get; private set; }

		#endregion

		#region public object PropertyValue { get; private set; }

		/// <summary>
		/// Returns the value of the changed property.
		/// </summary>
		public object PropertyValue { get; private set; }

		#endregion

	}

	/// <summary>
	/// The event argument class for the <see cref="PropertyTrackingServiceBase.WorkflowPropertiesRemoving"/> event.
	/// </summary>
	public sealed class WorkflowPropertiesRemovingEventArgs : EventArgs
	{
		/// <summary>
		/// Creates a new instance of the class and specifies the ID of the workflow instance
		/// whose properties are about to be removed.
		/// </summary>
		/// <param name="instanceId"></param>
		public WorkflowPropertiesRemovingEventArgs(Guid instanceId)
		{
			this.InstanceId = instanceId;
		}

		/// <summary>
		/// Returns the ID of the workflow instance.
		/// </summary>
		public Guid InstanceId { get; private set; }
	}

}
